﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Security;
using System.Collections.Generic;
using System.ComponentModel.Composition.Caching;
using System.ComponentModel.Composition.Caching.AttributedModel;
using System.ComponentModel.Composition.ReflectionModel;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using Microsoft.Internal;
#if !SILVERLIGHT
using System.Globalization;
#endif

namespace System.ComponentModel.Composition
{
    /// <summary>
    ///     An immutable ComposablePartCatalog created from a managed code assembly.
    /// </summary>
    /// <remarks>
    ///     This type is thread safe.
    /// </remarks>
    public class AttributedAssemblyPartCatalog : ComposablePartCatalog, ICachedComposablePartCatalog
    {
        private readonly object _thisLock = new object();
        private volatile Assembly _assembly = null;
        private volatile AttributedTypesPartCatalog _innerCatalog = null;
        private int _disposed = 0;
        private ComposablePartCatalog _cacheCatalog = null;
        private IDictionary<string, object> _cacheCatalogMetadata = null;

#if !SILVERLIGHT
        /// <summary>
        ///     Initializes a new instance of the <see cref="AttributedAssemblyPartCatalog"/> class 
        ///     with the specified code base.
        /// </summary>
        /// <param name="codeBase">
        ///     A <see cref="String"/> containing the code base of the assembly containing the
        ///     attributed <see cref="Type"/> objects to add to the <see cref="AttributedAssemblyPartCatalog"/>.
        /// </param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="codeBase"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///     <paramref name="codeBase"/> is a zero-length string, contains only white space, 
        ///     or contains one or more invalid characters as defined by <see cref="Path.InvalidPathChars"/>.
        /// </exception>
        /// <exception cref="PathTooLongException">
        ///     The specified path, file name, or both exceed the system-defined maximum length. 
        /// </exception>
        /// <exception cref="SecurityException">
        ///     The caller does not have path discovery permission. 
        /// </exception>
        /// <exception cref="FileNotFoundException">
        ///     <paramref name="codeBase"/> is not found.
        /// </exception>
        /// <exception cref="FileLoadException ">
        ///     <paramref name="codeBase"/> could not be loaded.
        ///     <para>
        ///         -or-
        ///     </para>
        ///     <paramref name="codeBase"/> specified a directory.
        /// </exception>
        /// <exception cref="BadImageFormatException">
        ///     <paramref name="codeBase"/> is not a valid assembly
        ///     -or- 
        ///     Version 2.0 or later of the common language runtime is currently loaded 
        ///     and <paramref name="codeBase"/> was compiled with a later version. 
        /// </exception>
        /// <remarks>
        ///     The assembly referenced by <paramref langword="codeBase"/> is loaded into the Load context.
        /// </remarks>
        public AttributedAssemblyPartCatalog(string codeBase)
        {
            Requires.NotNull(codeBase, "codeBase");

            AssemblyName assemblyName;
            try
            {
                assemblyName = AssemblyName.GetAssemblyName(codeBase);
            }
            catch (ArgumentException)
            {
                assemblyName = new AssemblyName();
                assemblyName.CodeBase = codeBase;
            }
            this._assembly = Assembly.Load(assemblyName);
        }
#endif

        /// <summary>
        ///     Initializes a new instance of the <see cref="AttributedAssemblyPartCatalog"/> class 
        ///     with the specified assembly.
        /// </summary>
        /// <param name="assembly">
        ///     The <see cref="Assembly"/> containing the attributed <see cref="Type"/> objects to 
        ///     add to the <see cref="AttributedAssemblyPartCatalog"/>.
        /// </param>
        /// <exception cref="ArgumentException">
        ///     <paramref name="assembly"/> is <see langword="null"/>.
        ///     <para>
        ///         -or-
        ///     </para>    
        ///     <paramref name="assembly"/> was loaded in the reflection-only context.
        /// </exception>
        public AttributedAssemblyPartCatalog(Assembly assembly)
        {
            Requires.NotNull(assembly, "assembly");

#if !SILVERLIGHT
            if (assembly.ReflectionOnly)
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Argument_AssemblyReflectionOnly, "assembly"), "assembly");
            }
#endif
            this._assembly = assembly;
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="AttributedAssemblyPartCatalog"/> class 
        ///     with the specified cache.
        /// </summary>
        /// <param name="cache">
        ///     The <see cref="ComposablePartCatalogCache"/> to read the catalog from.
        /// </param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="cache"/> is <see langword="null"/>.
        /// </exception>
        public AttributedAssemblyPartCatalog(ComposablePartCatalogCache cache)
        {
            Requires.NotNull(cache, "cache");

            // if the assembly hasn't been changed since the caching, then load the cache
            if (cache.Metadata.IsAssemblyCacheUpToDate())
            {
                this._cacheCatalogMetadata = cache.Metadata;
                this._cacheCatalog = cache.GetCacheCatalog(new AttributedComposablePartCatalogSite(this.CreatePartFromCache, delegate { }));
            }
            else
            {
                // just load the assembly given the information we have
                this._assembly = cache.Metadata.LoadAssemblyFromCache();
            }
        }

        /// <summary>
        ///    Caches the catalog using the specified cache writer
        /// </summary>
        /// <param name="writer">
        ///    The <see cref="ComposablePartCatalogCacheWriter"/> to use to cache the 
        ///    <see cref="AttributedAssemblyPartCatalog"/>.
        /// </param>
        /// <returns></returns>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AttributedAssemblyPartCatalog"/> has been disposed of.
        /// </exception>
        public object CacheCatalog(ComposablePartCatalogCacheWriter writer)
        {
            ThrowIfDisposed();

            Requires.NotNull(writer, "writer");
            
            IDictionary<string, object> metadata;
            if (this._cacheCatalogMetadata != null)
            {
                metadata = this._cacheCatalogMetadata;
            }
            else
            {
                Assumes.NotNull(this._assembly);
                metadata = new Dictionary<string, object>();
                this._assembly.CacheAssembly(metadata);
            }

            object cacheToken = writer.WriteCache(
                this.GetType(),
                this.Parts,
                metadata,
                new AttributedComposablePartCatalogSite(this.CreatePartFromCache, delegate { }));

            writer.WriteRootCacheToken(cacheToken);
            return cacheToken;
        }

        /// <summary>
        ///     Gets a value indicating whether the underlying cache is up-to-date.
        /// </summary>
        /// <value>
        ///     <see langword="true"/> if the underlying <see cref="ComposablePartCatalogCache"/> 
        ///     is up-to-date; otherwise, <see langword="false"/>.
        /// </value>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AttributedAssemblyPartCatalog"/> has been disposed of.
        /// </exception>
        public bool IsCacheUpToDate
        {
            get
            {
                ThrowIfDisposed();

                return (this._cacheCatalog != null);
            }
        }

        /// <summary>
        ///     Gets the part definitions of the catalog.
        /// </summary>
        /// <value>
        ///     A <see cref="IQueryable{T}"/> of <see cref="ComposablePartDefinition"/> objects of the 
        ///     <see cref="AttributedAssemblyPartCatalog"/>.
        /// </value>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AttributedAssemblyPartCatalog"/> has been disposed of.
        /// </exception>
        public override IQueryable<ComposablePartDefinition> Parts
        {
            get
            {
                ThrowIfDisposed();

                // If the cache is present, we read from that
                if (this._cacheCatalog != null)
                {
                    return this._cacheCatalog.Parts;
                }

                if (this._innerCatalog == null)
                {
                    lock (this._thisLock)
                    {
                        if (this._innerCatalog == null)
                        {
                            var catalog = new AttributedTypesPartCatalog(this._assembly.GetTypes());
                            this._innerCatalog = catalog;
                        }
                    }
                }
                return this._innerCatalog.Parts;
            }
        }

        /// <summary>
        ///     Returns the export definitions that match the conditions defined by the specified constraint.
        /// </summary>
        /// <param name="constraint">
        ///     A <see cref="Expression{TDelegate}"/> containing a <see cref="Func{T, TResult}"/> 
        ///     that defines the conditions of the <see cref="ExportDefinition"/> objects to return.
        /// </param>
        /// <returns>
        ///     An <see cref="IEnumerable{T}"/> of <see cref="Tuple{TFirst, TSecond}"/> containing the 
        ///     <see cref="ExportDefinition"/> objects and their associated 
        ///     <see cref="ComposablePartDefinition"/> objects that matche the conditions defined 
        ///     by <paramref name="constraint"/>.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="constraint"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AttributedTypesPartCatalog"/> has been disposed of.
        /// </exception>
        public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(Expression<Func<ExportDefinition, bool>> constraint)
        {
            ThrowIfDisposed();

            Requires.NotNull(constraint, "constraint");

            // If the cache is present, we read from that
            if (this._cacheCatalog != null)
            {
                return this._cacheCatalog.GetExports(constraint);
            }
            else
            {
                return base.GetExports(constraint);
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (Interlocked.CompareExchange(ref this._disposed, 1, 0) == 0)
            {
                try
                {
                    if (disposing)
                    {
                        if (this._innerCatalog != null)
                        {
                            this._innerCatalog.Dispose();
                        }

                        if (this._cacheCatalog != null)
                        {
                            this._cacheCatalog.Dispose();
                        }
                    }
                }
                finally
                {
                    base.Dispose(disposing);
                }
            }
        }

        private ReflectionComposablePart CreatePartFromCache(CachedAttributedComposablePartDefinition definition)
        {
            // This is called through the site when reading the cache. We need to load the assembly 
            // - preferably only once - and then read the type.

            if (this._assembly == null)
            {
                lock (this._thisLock)
                {
                    if (this._assembly == null)
                    {
                        Assumes.NotNull(this._cacheCatalogMetadata);
                        Assembly assembly = this._cacheCatalogMetadata.LoadAssemblyFromCache();
                        this._assembly = assembly;
                    }
                }
            }

            return new CachedAttributedComposablePart(definition);
        }

        private void ThrowIfDisposed()
        {
            if (this._disposed == 1)
            {
                throw new ObjectDisposedException(this.GetType().ToString());
            }
        }
    }
}
